Receiver-side price floor for single-node store admissions#176
Receiver-side price floor for single-node store admissions#176grumbach wants to merge 3 commits into
Conversation
…ions ADR-0004 binds every quote's price to its issuer's committed key count — a price CEILING. Removing the old local floor (b52f457) left no revenue floor at all: the node verifier accepts 1..=7 quote proofs and fully validates only the median-priced candidate, so a modified client can fetch quotes from the whole neighbourhood, settle only the cheapest valid one 3x, and every receiver accepts. The flexible-bundle change (WithAutonomi#136) was safe while the old floor stood; their combination is the hole, and the one-quote client (ant-client WithAutonomi#150) makes that path routine. Add the floor half, priced from the SAME live commitment the receiver's own QuoteGenerator uses (committed responsible key count via a new non-mutating CommitmentSource::current_binding_snapshot — never current_chunks(), whose divergence from commitment counts is what false-rejected honest quotes under the pre-ADR-0004 floor; and never current_binding_for_quote(), which would extend answerability). Requirement on single-node store admissions: settled >= 3 x tolerance% x calculate_price(local committed key count) alongside the existing settled >= 3 x median check, i.e. an honest client may overpay a cheap quote to clear stricter receivers. No live commitment (fresh node, retired, restart window, unwired) prices the floor at baseline — vacuous, never an outage mode. Shadow-first rollout: default is observe-only with one telemetry line per evaluated admission (target ant_node::payment::price_floor) recording paid price, settled amount, local count/price, and the would-reject verdict. Enforcement is per-node via ANT_PRICE_FLOOR_ENFORCE=1 for canary rollout (tolerance override: ANT_PRICE_FLOOR_TOLERANCE_PERCENT, default 50); unsetting it is the kill switch. Clients need 4-of-20 stores, so canary enforcement cannot fail honest uploads while telemetry calibrates the tolerance. A floor rejection is an economic admission decision only, never trust evidence. Scope: split VerificationContext::FreshReplication out of ClientPut — verified identically everywhere (store-strength cache, cross-check, first-audit) so the floor's telemetry can distinguish direct ingress from fan-out and the fan-out policy stays tunable without touching direct PUTs. The floor applies to both (one cheap accepting node must not fan a below-floor proof around everyone else's floor) and NEVER to paid-list admission, repair, or cache hits — no historical receipt is repriced. Merkle proofs are untouched. Tests: cheapest-of-K settlement rejected under enforcement (direct PUT and fresh replication), exactly-at-floor overpayment accepted, shadow mode never rejects, no-commitment receivers stay baseline-vacuous, paid-list admission never repriced.
There was a problem hiding this comment.
Pull request overview
Adds a receiver-side “price floor” policy to single-node payment verification so a node can reject (or shadow-log) underpaid store admissions even when a client settles only the cheapest valid quote from a bundle.
Changes:
- Introduces
PriceFloorConfig(shadow by default; env-controlled enforcement + tolerance) and applies it during single-node proof verification for store-admission contexts. - Splits
VerificationContextwith a newFreshReplicationvariant and unifies cache/admission semantics viais_store_admission(). - Adds
CommitmentSource::current_binding_snapshot()and wires the same commitment source into both quoting and the verifier’s price-floor computation; adds unit/e2e config wiring and tests.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/e2e/testnet.rs | Adds default price_floor config into e2e testnet verifier construction. |
| tests/e2e/data_types/chunk.rs | Wires PriceFloorConfig::default() into verifier config used by chunk e2e tests. |
| src/storage/handler.rs | Forwards the attached commitment source to the payment verifier (in addition to the quote generator). |
| src/replication/mod.rs | Uses VerificationContext::FreshReplication for fresh-offer payment verification and updates the associated test. |
| src/replication/commitment_state.rs | Implements current_binding_snapshot() for ResponderCommitmentState without mutating answerability. |
| src/payment/verifier.rs | Implements PriceFloorConfig, adds store-admission context predicate, enforces/logs receiver-side floor after settlement verification, and adds targeted tests. |
| src/payment/quote.rs | Extends CommitmentSource trait with current_binding_snapshot() and updates test implementation. |
| src/payment/mod.rs | Re-exports PriceFloorConfig and the env var names for external configuration. |
| src/node.rs | Initializes verifier price_floor from env and logs when enforcement is enabled. |
| src/devnet.rs | Sets default price_floor config for devnet verifier construction. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let local_key_count = source | ||
| .and_then(|s| s.current_binding_snapshot()) | ||
| .map_or(0u32, |binding| binding.key_count); | ||
| let local_price = calculate_price(usize::try_from(local_key_count).unwrap_or(usize::MAX)); | ||
|
|
The floor is its own economic policy, not part of ADR-0004's ceiling: it amends "a node may always charge less" to "a receiver may refuse a payment that settles below its own commitment-priced floor". Records the pricing basis (committed responsible count via non-mutating snapshot), the scope (store admissions only, no historical repricing), the shadow-first rollout with per-node canary enforcement, and the validation gates.
Adds a live-Anvil integration suite (poc_price_floor_live) that exercises the REAL production paths the unit-test overrides cannot: a real deployed payment vault paid with a real on-chain pay_for_quotes, read back through the production completedPayments contract call, against the real ResponderCommitmentState as the floor's commitment source. Proves shadow accepts a below-floor settlement, enforcement rejects the cheapest-of-K one-quote settlement, an at-floor overpayment is accepted, and reading the floor does not mutate commitment state. Hardening from adversarial xhigh review (verdict: GO for shadow deployment; no admission-logic fix required): - from_env now FAILS CLOSED on a present-but-invalid tolerance: a malformed or out-of-range ANT_PRICE_FLOOR_TOLERANCE_PERCENT can no longer leave enforcement on with a silently-defaulted 50% (it disables enforcement and logs an error). Unit test covers unset/valid/out-of-range/unparseable. - Extend the two 'every context' regression tests to include FreshReplication, matching the store-admission predicate. - Correct stale comments that still claimed the floor reads current_chunks() / the on-disk record count (handler.rs, verifier.rs storage field + attach doc). - Soften the 'never an outage' wording: a no-commitment baseline floor clears every on-curve honest 3x settlement; only an off-curve sub-baseline quote (rejected by the ADR-0004 arithmetic gate once enforced) could fall below it.
| /// Idempotent: calling twice replaces the handle. | ||
| pub fn attach_storage(&self, storage: Arc<LmdbStorage>) { | ||
| *self.storage.write() = Some(storage); | ||
| debug!("PaymentVerifier: LmdbStorage attached for paid-quote price-floor checks"); |
| fn price_floor_from_env_fails_closed_on_invalid_tolerance() { | ||
| // Defaults with nothing set. | ||
| std::env::remove_var(PRICE_FLOOR_ENFORCE_ENV); | ||
| std::env::remove_var(PRICE_FLOOR_TOLERANCE_ENV); |
| .map(|i| { | ||
| let mut k = [0u8; 32]; | ||
| k[..4].copy_from_slice(&i.to_be_bytes()); | ||
| (k, *blake3::hash(&k).as_bytes()) |
dirvine
left a comment
There was a problem hiding this comment.
Review — receiver-side price floor
Reviewed current head bfe16d733c9be1ca887db3097b2e7acbb9a3d52b with six independent passes and local verification.
Verdict: no protocol/security blocker for merging the default-shadow implementation. This is not approval to enable enforcement yet.
What holds
ClientPutandFreshReplicationboth use store-strength verification and floor evaluation;PaidListAdmission, repair, Merkle proofs and existing store-cache hits remain deliberately exempt.- Paid-list cache entries cannot satisfy store admission, and insertion happens only after the authenticated paid quote, on-chain settlement and floor checks succeed.
- The floor uses the receiver's non-mutating live commitment snapshot and compares the actual settled amount, not the cheap quote price.
- Commitment rotation/snapshot locking is sound; no context/cache downgrade bypass was found.
- The focused local floor suite passed:
cargo test price_floor --lib --features test-utils— 7/7. - Current GitHub checks are all green across builds, platform tests, clippy, fmt, docs, audit, ADR validation and no-logging tests.
Rollout gates before any canary enforcement
- Make shadow evidence observable.
ant_node::payment::price_flooris aninfo!event, but normal startup installs no subscriber unless--enable-logging/ANT_ENABLE_LOGGINGis set, and no-logging builds cannot emit it. Confirm a logging-enabled artefact and service configuration before calling shadow collection active. - Make the signal queryable. The values are currently embedded in one high-volume formatted string containing the full XOR name. Prefer structured low-cardinality fields/counters/histograms (separate
ClientPutandFreshReplication) or publish and test the exact collector parser/query. - Run the live settlement suite with retained evidence.
poc_price_floor_livegenuinely covers a real Anvil payment, productioncompletedPaymentsread and real commitment state, but no PR workflow executes it. Add one Ubuntu CI invocation, or run and retain equivalent evidence, before enforcement. It deliberately bypasses live DHT issuer locality and production startup/network wiring, so describe it as a focused vault/floor integration test rather than a full network E2E. - Soften the 4-of-20 claim. Compatible clients require four acceptances and can fall back across up to twenty discovered targets; this gives headroom but does not guarantee success when fewer targets are usable or enforcing nodes are XOR-local. Replace “cannot fail honest uploads” with a conditional statement and define the maximum canary count.
- Document rollback accurately. The environment is read once at startup. The kill switch is: unset
ANT_PRICE_FLOOR_ENFORCE, restart the named canary, then positively verify shadow mode. Unsetting alone does nothing to a running node. - Define the shadow window, sample size, rejection bound, canary count, stop condition and sign-off owner in ADR-0006 before moving it from Proposed or widening enforcement. “~0” is not an operational threshold.
Minor, non-blocking notes
- The arithmetic computes
(local_price × tolerance / 100) × 3; the written formula can be read aslocal_price × tolerance × 3 / 100. The difference is at most two atomic units, but code/ADR/tests should name one canonical rounding rule. - Present-but-non-Unicode tolerance values are treated as absent, and unrecognised enforcement values silently select shadow mode. Availability-safe, but an explicit effective-config startup signal would make operator mistakes visible.
- The existing process-global environment test would be safer behind a static mutex.
Review action: comment only — no changes requested, no approval/merge action. Human review is still required for the Proposed ADR and enforcement policy.
Summary
Node-side companion to ant-client #150 (one-quote uploads). ADR-0004 gives every quote a price ceiling (price = f(committed key count)); removing the old local floor (
b52f4577) left no revenue floor. Since the verifier accepts 1..=7 quote proofs and fully validates only the median candidate, a modified client can fetch quotes from the whole neighbourhood and settle only the cheapest valid one. The flexible bundles from #136 were safe while the old floor stood; the combination is the hole, and one-quote clients make it routine.This PR restores the floor on the receiver, priced from the receiver's own live commitment:
alongside the existing
settled >= 3 x mediancheck. The floor compares the settled amount, so an honest client may overpay a cheap quote to clear stricter receivers.Design points
CommitmentSource::current_binding_snapshot()— the exact sourceQuoteGeneratorprices from, wired inAntProtocol::attach_commitment_source. Nevercurrent_chunks()(the unlike-counts comparison that false-rejected honest quotes under the pre-ADR-0004 floor), and nevercurrent_binding_for_quote()(which would extend commitment answerability as a side effect).calculate_price(0), making it vacuous rather than an outage mode.ant_node::payment::price_floor) with paid price, settled amount, local count/price, and the would-reject verdict. Enforcement is a per-node canary opt-in viaANT_PRICE_FLOOR_ENFORCE=1(tolerance overrideANT_PRICE_FLOOR_TOLERANCE_PERCENT, default 50%); unsetting it is the kill switch. Clients need 4-of-20 stores, so canary enforcement cannot fail honest uploads while telemetry calibrates the tolerance.VerificationContext::FreshReplicationsplits fan-out from direct PUT — verified identically everywhere (store-strength cache, cross-check, first-audit) so behaviour is unchanged; the floor applies to both (a single cheap accepting node must not fan a below-floor proof around everyone else's floor), and never to paid-list admission, repair, or cache hits. No historical receipt is repriced. Merkle proofs untouched.Tests
-D warnings), fmt, and docs clean.Rollout
price_floortelemetry for the settled/local-price ratio distribution.